package org.fujene.diskblock;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;

import javassist.NotFoundException;

import org.fujene.constants.Constants;
import org.fujene.memoryblock.MemoryBlock;
import org.fujene.toolbox.ByteConverter;

public abstract class DiskBlock{
	// Add constants to here...
	protected static final int HEADERLEN = 8;

	protected static final int PTR = Constants.BYTE_GLOBALPTR;
	protected static final int RELPTR = Constants.BYTE_RELATIVEPTR;
	protected static final int STRLEN = Constants.BYTE_STRLEN;
	protected static final int DOCFREQ = Constants.BYTE_DOCFREQ;
	protected static final int TERMFREQ = Constants.BYTE_TERMFREQ;
	protected static final int IDDIFF = Constants.BYTE_INVINDEXDIFF;

	private int ID;
	private int type;
	protected int optionSize;
	protected int bodySize;
	protected int bodyVacancy;
	protected byte version;
	protected File fileDesc;
	private MemoryBlock memBlock;

	public DiskBlock(MemoryBlock memBlock){
		this.version = Constants.FINALVER;

		this.ID = memBlock.getID();
		this.type = memBlock.getType();
		this.optionSize = memBlock.getOptionSize();
		this.bodySize = memBlock.getBodySize();
		this.bodyVacancy = memBlock.getBodyVacancy();
		this.memBlock = memBlock;
	}

	// Make block with the block
	public DiskBlock(MemoryBlock memBlock, String dir){
		this(memBlock);

		this.fileDesc = new File(dir + memBlock.getID() + ".blk");
	}

	public DiskBlock(String fileName) throws NotFoundException{
		this(new File(fileName));
	}

	public DiskBlock(File fileDesc) throws NotFoundException{
		this.version = Constants.FINALVER;
		this.fileDesc = fileDesc;
		if(fileDesc == null) throw new NotFoundException("No disk block specified.");

		int validate = readRequiredHeaders();

		if(validate != 0){ return; }
	}

	public DiskBlock(int ID, File fileDesc, int type, int defaultBlockSize, int defaultOptionSize){
		this.version = Constants.FINALVER;

		this.fileDesc = fileDesc;

		// put defoult values to here...
		this.ID = ID;
		this.type = type;
		this.bodySize = defaultBlockSize;
		this.optionSize = defaultOptionSize;
	}

	private int readRequiredHeaders(FileInputStream stream) throws IOException{
		byte[] tempb = new byte[4];

		stream.read(tempb);
		if(!verifyVersion(tempb)) return -2;
		stream.read(tempb);
		this.ID = ByteConverter.byteToInt(tempb);
		stream.read(tempb);
		this.type = ByteConverter.byteToInt(tempb);
		stream.read(tempb);
		this.optionSize = ByteConverter.byteToInt(tempb);
		stream.read(tempb);
		this.bodySize = ByteConverter.byteToInt(tempb);
		stream.read(tempb);
		this.bodyVacancy = ByteConverter.byteToInt(tempb);

		return 0;
	}

	public int getID(){
		return ID;
	}

	public int getType(){
		return type;
	}

	public String getTypeStr(){
		switch(this.type){
		case Constants.MEMORY_TYPE_DICT:
			return "Dictionary";
		case Constants.MEMORY_TYPE_INVINDEX:
			return "Invert Index";
		case Constants.MEMORY_TYPE_CONTENTS:
			return "Contents";
		case Constants.MEMORY_TYPE_SKIPPTRCONT:
			return "Skip Pointer for Content";
		case Constants.MEMORY_TYPE_SKIPPTR:
			return "Skip Pointer";
		case Constants.MEMORY_TYPE_APPENDIX:
			return "Supplements";
		default:
			break;
		}

		return "Unknown";
	}

	protected int readRequiredHeaders(){
		FileInputStream stream = null;
		int retval = -3;
		try{
			stream = new FileInputStream(fileDesc);
			retval = readRequiredHeaders(stream);
		}catch(FileNotFoundException e){}catch(IOException e){}finally{
			try{
				if(stream != null) stream.close();
			}catch(IOException e){}
		}

		return retval;
	}

	private boolean verifyVersion(byte[] head){
		// Check whether block was made on the latest version: "far"42
		if(head[0] != 'f') return false;
		if(head[1] != 'a') return false;
		if(head[2] != 'r') return false;
		if(head[3] < Constants.FINALVER) return false;
		else{
			this.version = head[3];
		}

		return true;
	}

	// Most process is only to read
	protected int getOption(int index){
		if(index >= optionSize) return 0;

		RandomAccessFile inputs = null;
		int retval = -3;
		byte[] b = new byte[4];
		try{
			inputs = new RandomAccessFile(fileDesc, "r");
			switch(version){
			case Constants.FINALVER: // for back compatibility
				inputs.seek((4 * HEADERLEN) + (4 * index));
				inputs.read(b);
				retval = ByteConverter.byteToInt(b);
				break;

			default:
				break;
			}
		}catch(FileNotFoundException e){}catch(IOException e){}finally{
			try{
				if(inputs != null) inputs.close();
			}catch(IOException e){}
		}

		return retval;
	}

	public byte[] getBody(int from, int length){
		if(from + length > bodySize) return null;

		RandomAccessFile inputs = null;
		byte[] val = new byte[length];
		try{
			inputs = new RandomAccessFile(fileDesc, "r");
			switch(version){
			case Constants.FINALVER: // for back compatibility
				inputs.seek((4 * HEADERLEN) + (4 * optionSize) + from);
				inputs.read(val);
				break;

			default:
				break;
			}
		}catch(FileNotFoundException ignored){}catch(IOException ignored){}finally{
			try{
				if(inputs != null) inputs.close();
			}catch(IOException ignored){}
		}

		return val;
	}

	public abstract int toMemory(MemoryBlock memBlock);

	public abstract byte[] getData();

	// Create file
	public void save(int from, int to, String newDir){
		System.out.println("Store disk " + from + " -> " + to);
		if(memBlock != null){
			FileOutputStream os = null;

			try{
				os = new FileOutputStream(fileDesc);

				// header
				os.write(new byte[]{ 'f', 'a', 'r', version });
				os.write(ByteConverter.intToByte(this.ID, 4));
				os.write(ByteConverter.intToByte(this.type, 4));
				os.write(ByteConverter.intToByte(this.optionSize, 4));
				os.write(ByteConverter.intToByte(this.bodySize, 4));
				os.write(ByteConverter.intToByte(this.bodyVacancy, 4));
				os.write(ByteConverter.intToByte(0, 4));
				os.write(ByteConverter.intToByte(0, 4));

				// option
				os.write(this.memBlock.getOptionByteStream());

				// body
				os.write(this.memBlock.getBody(from, from + to));
			}catch(FileNotFoundException e){
				System.err.println("ERROR: File cannot open.");
			}catch(IOException ignored){}finally{
				try{
					if(os != null) os.close();
				}catch(IOException ignored){}
			}
		}
	}

	public File getDesc(){
		return fileDesc;
	}
	
	public void copy(String destDir) throws IOException {
		if(!destDir.endsWith("/")) destDir.concat("/");
		File destFile = new File(destDir + this.ID + ".blk");
		if(destFile.exists()) destFile.createNewFile();
		FileChannel sourceChannel = null, destinationChannel = null;
		try{
			sourceChannel = new FileInputStream(fileDesc).getChannel();
			destinationChannel = new FileOutputStream(destFile).getChannel();
			sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel);
		}finally{
			if(sourceChannel != null) sourceChannel.close();
			if(destinationChannel != null) destinationChannel.close();
		}
	}
}